#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>
#include <ctype.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define NUM 1024
#define OPT_NUM 64  // 命令行参数的最多个数
#define NONE_REDIR 0    // 无重定向
#define INPUT_REDIR 1   // 输入重定向
#define OUTPUT_REDIR 2  // 输出重定向
#define APPEDN_REDIR 3  // 追加重定向

// 过滤空格
#define trimSpace(start) do{ while(isspace(*start))  ++start; }while(0)

char lineCommand[NUM];
char* myargv[OPT_NUM];
// 上一个进程的退出信息
int lastCode = 0;
int lastSignal = 0;

int redirType = NONE_REDIR; // 重定向类型默认为无重定向
char* redirFile = NULL;     // 重定向的文件名

// "ls -a -l > myfile.txt" -> "ls -a -l" "myfile.txt"
void commandCheck(char* commands)
{
    // 重置重定向信息
    redirType = NONE_REDIR;
    redirFile = NULL;
    errno = 0;

    assert(commands);
    char* start = commands;
    char* end = commands + strlen(commands);

    while(start < end)
    {
        if(*start == '>')
        {
            *start = '\0';
            ++start;
            if(*start == '>')
            {
                // "ls -a >> myfile.txt"
                redirType = APPEDN_REDIR;   // 追加重定向
                ++start;
            }
            else
            {
                // "ls -a > myfile.txt"
                redirType = OUTPUT_REDIR;   // 输出重定向
            }
            trimSpace(start);   // 过滤空格
            redirFile = start;
            break;
        }
        else if(*start == '<')
        {
            // "cat <     myfile.txt"
            *start = '\0';  // 将字符串分割成两部分
            ++start;
            trimSpace(start);   // 过滤空格
            // 填写重定向信息
            redirType = INPUT_REDIR;    // 输入重定向
            redirFile = start;
            break;
        }
        else
        {
            ++start;
        }
    }
}

int main()
{
    while(1)
    {
        char* user = getenv("USER");
        // 根据用户输出对应的提示信息, get_current_dir_name函数可以获得当前的工作路径
        if(strcmp(user, "root") == 0)
        {
            printf("[%s@%s %s]# ", user, getenv("HOSTNAME"), get_current_dir_name());
        }
        else
        {
            printf("[%s@%s %s]$ ", user, getenv("HOSTNAME"), get_current_dir_name());
        }
        fflush(stdout); // 刷新缓冲区
        
        // 获取用户输入
        char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
        assert(s != NULL);
        // 清除最后一个\n, abcd\n
        lineCommand[strlen(lineCommand) - 1] = 0;

        // 字符串切割:"ls -a -l" -> "ls" "-a" "-l"
        // "ls -a -l > myfile.txt" -> "ls -a -l" "myfile.txt"
        // "cat < myfile.txt" -> "cat" "myfile.txt"
        commandCheck(lineCommand);  // 如果有重定向,则将字符串拆成两部分
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;
        // 因为无法执行"ll"指令, 所以这里做一下处理
        if(myargv[0] != NULL && strcmp(myargv[0], "ll") == 0)
        {
            myargv[0] = "ls";
            myargv[i++] = "-l";
        }
        if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
        {
            myargv[i++] = "--color=auto";
        }
        // 如果切割完毕, strtok返回NULL, myargv[end] = NULL
        while(myargv[i++] = strtok(NULL, " "));

        // 如果是cd命令, 不需要创建子进程来执行, 让当前进程的父进程shell执行对应的命令, 本质就是调用系统接口
        // 像这种不需要创建子进程来执行, 而是让shell自己执行的命令, 称为内建命令或者内置命令
        // echo和cd就是一个内建命令
        if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            // 如果cd命令没有第二个参数, 则切换到家目录
            if(myargv[1] == NULL)
            {
                chdir(getenv("HOME"));  // 更改到家目录
            }
            else
            {
                if(strcmp(myargv[1], "-") == 0) // 该功能还有BUG, 因为环境变量的问题
                {
                    chdir(getenv("OLDPWD"));    // 回到上一次所处的路径
                }
                else if(strcmp(myargv[1], "~") == 0)
                {
                    chdir(getenv("HOME"));  // 去到家目录
                }
                else
                {
                    chdir(myargv[1]);   // 更改到指定目录
                }
            }
            continue;   // 不创建子进程, continue回到while循环处
        }

        // 实现echo命令, 当前的echo命令功能也不是很全
        if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
        {
            if(strcmp(myargv[1], "$?") == 0)
            {
                printf("%d, %d\n", lastSignal, lastCode);
            }
            else
            {
                printf("%s\n", myargv[1]);
            }
            continue;
        }

        // 创建子进程来执行命令
        pid_t id = fork();
        assert(id != -1);

        // child process
        if(id == 0)
        {
            // 因为命令是子进程执行的,真正重定向的工作一定是子进程执行的
            // 如何重定向,是父进程要个子进程提供信息
            // 这里的重定向不会影响父进程,因为进程具有独立性
            switch(redirType)
            {
                case NONE_REDIR:
                    // 什么都不做
                    break;
                case INPUT_REDIR:
                    {
                        ssize_t fd = open(redirFile, O_RDONLY);
                        if(fd < 0)
                        {
                            perror("open");
                            exit(errno);
                        }
                        // 重定向的文件已经成功打开了
                        dup2(fd, 0);
                    }
                    break;
                case OUTPUT_REDIR:
                case APPEDN_REDIR:
                    {
                        int flags = O_WRONLY | O_CREAT;
                        if(redirType == APPEDN_REDIR)   flags |= O_APPEND;
                        else flags |= O_TRUNC;
                        ssize_t fd = open(redirFile, flags, 0666);
                        if(fd < 0)
                        {
                            perror("open");
                            exit(errno);
                        }
                        dup2(fd, 1);    // ls等指令执行结果是打印在显示器上的
                    }
                    break;
                default:
                    printf("error\n");
                    break;
            }
            execvp(myargv[0], myargv);  // 执行程序替换的时候,不会影响曾经进程打开的重定向的文件,因为程序替换只是替换代码和数据
            exit(errno);    // 进程替换失败
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);   // 阻塞等待
        assert(ret > 0);
        lastCode = ((status >> 8) & 0xFF);
        lastSignal = (status & 0x7F);
    }
    return 0;
}
